//
//  FlexboxLayout.swift
//  MacDemo
//
//  Created by FH on 2020/8/17.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

#if os(OSX)
import Cocoa
typealias FLView = NSView
#else
import UIKit
typealias FLView = UIView
#endif

let FLNotify_OnFocus_Key = "FLNotify_OnFocus_Key".toNotificationName()

// REMARK: common methods

func calculateGridCellDim(gridCount: Int, gridViewSize: CGSize, cellMargin: CGFloat, extraInfoHeight: CGFloat) -> CGFloat {
    let sizeWidth = gridViewSize.width
    let sizeHeight = gridViewSize.height
    let isMainAxisWidth = sizeWidth >= sizeHeight
    let mainAxisDim = isMainAxisWidth ? sizeWidth : sizeHeight
    let crossAxisDim = isMainAxisWidth ? sizeHeight : sizeWidth
    let mainAxisExtraDim = CGFloat(isMainAxisWidth ? cellMargin : (extraInfoHeight + cellMargin))
    let crossAxisExtraDim = CGFloat(isMainAxisWidth ? (extraInfoHeight + cellMargin) : cellMargin)
    if gridCount == 1 {
        let width = sizeWidth - mainAxisExtraDim
        let height = sizeHeight - crossAxisExtraDim
        return width > height ? height : width
    }
    
    let remainDim = mainAxisDim - crossAxisDim
    // row/column基本值
    let number = CGFloat(Int(sqrt(Double(gridCount))))
    // row/column会多出来的cell数量
    var addMoreNumber = CGFloat(Int(number * remainDim / crossAxisDim))
    
    var dim: CGFloat = 0
    var isDecrease = true
    while true {
        let currNum = number + addMoreNumber
        var currDim = (mainAxisDim - currNum * mainAxisExtraDim) / currNum
        // 四舍五入处理，防止cell数量太多时，因为dim小数部分累加后导致一行留空太多
        let integerDim = CGFloat(Int(currDim))
        var decimalDim = currDim - integerDim
        if decimalDim > 0.5 || decimalDim < 0.4 {
            decimalDim = CGFloat(floor(Double(decimalDim * 10)) / 10)
            currDim = integerDim + decimalDim
        } else if decimalDim >= 0.4 {
            currDim = integerDim + 0.35
        }
        let currCrossNum = CGFloat(Int(sizeHeight / (currDim + crossAxisExtraDim)))
        let currTotalNum = currNum * currCrossNum
        // 判断是要减少dim还是增加dim，以达到尽量填充width且不超出height的目的
        if currTotalNum >= CGFloat(gridCount) {
            dim = currDim
            if !isDecrease {
                break
            }
        } else {
            if dim == 0 {
                isDecrease = false
            }
            if isDecrease {
                break
            }
        }
        if isDecrease {
            addMoreNumber -= 1
        } else {
            addMoreNumber += 1
        }
    }
    return dim
}

// REMARK: extenions

extension Int {
    func toFLValue() -> FLValue {
        return FLValue(self)
    }
}

extension Float {
    func toFLValue() -> FLValue {
        return FLValue(self)
    }
}

extension Double {
    func toFLValue() -> FLValue {
        return FLValue(self)
    }
}

extension CGFloat {
    func toFLValue() -> FLValue {
        return FLValue(self)
    }
}

// REMARK: controls

#if os(OSX)

enum FLViewEventType {
    case leftMouseDown
    case leftMouseDoubleDown
    case rightMouseDown
    case rightMouseDoubleDown
    case other
}

extension NSEvent {
    func getViewEventType() -> FLViewEventType {
        // TODO: add debounce let delayThreshold = 0.15
        if self.type == .leftMouseDown {
            if self.clickCount > 1 {
                return .leftMouseDoubleDown
            } else {
                return .leftMouseDown
            }
        } else if self.type == .rightMouseDown {
            if self.clickCount > 1 {
                return .rightMouseDoubleDown
            } else {
                return .rightMouseDown
            }
        }
        return .other
    }
}

class FLCommonView : NSView {
    let mutex = Mutex()
    var callbacks: [FLViewEventType: [UUID: Action1<NSEvent>]] = [:]
    
    func rac_observerEvent(_ eventType: FLViewEventType) -> IOMonad<(NSView, NSEvent)> {
        return IOMonad {[weak self] thunker in
            var callbackId = UUID()
            if let self = self {
                callbackId = self.addObserver(eventType: eventType) {[weak self] event in
                    if let this = self {
                        thunker.sendNext((this, event))
                    }
                }
            }
            return {
                if let self = self {
                    let succ = self.mutex.tryLock()
                    self.removeObserver(eventType: eventType, callbackId: callbackId)
                    if succ {
                        self.mutex.unlock()
                    }
                }
            }
        }.take(until: self.rac_willDealloc)
    }
    
    private func addObserver(eventType: FLViewEventType, callbackFn: @escaping Action1<NSEvent>) -> UUID {
        let succ = mutex.tryLock()
        let callbackId = UUID()
        var currCallbacks = self.callbacks[eventType]
        if currCallbacks == nil {
            currCallbacks = [UUID: Action1<NSEvent>]()
        }
        currCallbacks?[callbackId] = callbackFn
        self.callbacks[eventType] = currCallbacks!
        
        defer {
            if succ {
                mutex.unlock()
            }
        }
        return callbackId
    }
    
    private func removeObserver(eventType: FLViewEventType, callbackId: UUID) {
        let succ = mutex.tryLock()
        if var currCallbacks = self.callbacks[eventType] {
            currCallbacks.removeValue(forKey: callbackId)
            self.callbacks[eventType] = currCallbacks
        }
        if succ {
            mutex.unlock()
        }
    }
    
    private func onEventCallback(event: NSEvent) {
        if self.callbacks.count > 0 {
            let succ = mutex.tryLock()
            if let currCallbacks = self.callbacks[event.getViewEventType()] {
                for (_, callback) in currCallbacks {
                    callback(event)
                }
            }
            if succ {
                mutex.unlock()
            }
        }
    }
    
    convenience init() {
        self.init(frame: CGRect.zero)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func mouseDown(with event: NSEvent) {
        self.onEventCallback(event: event)
    }
    
    override func rightMouseDown(with event: NSEvent) {
        self.onEventCallback(event: event)
    }
    
    final func bindLayout(to ctx: FLContext, _ superView: NSView, _ settingFn: (FLNode) -> Void) {
        self.bind(to: ctx, superView, settingFn)
        self.layoutUI(yoga: ctx)
    }
    
    func layoutUI(yoga: FLContext) { }
}

extension NSViewController {
    func dismissViewController(_ viewController: NSViewController) {
        self.dismiss(viewController)
        viewController.view.window?.close()
    }
}

extension NSView {
    static func scaleOfScreen() -> Float {
        return Float(NSScreen.main?.backingScaleFactor ?? 2)
    }
    
    open override var description: String {
        return "\(super.description), frame:\(self.frame), tag=\(self.flTag)"
    }
    
    // NSView.tag is readonly property
    var flTag: Int {
        get {
            return Int(self.identifier?.rawValue ?? "0") ?? 0
        }
        set {
            self.identifier = NSUserInterfaceItemIdentifier("\(newValue)")
        }
    }
    
    var flCornerRadius: CGFloat {
        get {
            return self.layer?.cornerRadius ?? 0
        }
        set {
            self.layer?.cornerRadius = newValue
        }
    }
    
    var flBackgroundColor: NSColor {
        get {
            return self.layer?.backgroundColor.flatMap { NSColor(cgColor: $0) } ?? .clear
        }
        set {
            self.layer?.backgroundColor = newValue.cgColor
        }
    }
    
    var isFLContentNode: Bool {
        return self is NSTextField || self is NSTextView || self is NSButton || self is NSImageView
    }
    
    func sizeThatFits(_ size: CGSize) -> CGSize {
        if self.isFLContentNode {
            var fittingSize = self.fittingSize.toCGSize()
            if let lbl = self as? NSTextField, lbl.stringValue.count == 0 {
                fittingSize = CGSize.zero
            }
            return CGSize(width: min(size.width, fittingSize.width),
                          height: min(size.height, fittingSize.height))
        } else {
            return CGSize(width: 0, height: 0)
        }
    }
    
    @discardableResult
    func bind(to ctx: FLContext, _ superView: NSView, _ settingFn: (FLNode) -> Void = { _ in }) -> FLNode {
        self.wantsLayer = true
        self.layer?.masksToBounds = false
        let node = ctx.associate(view: self, superView: superView)
        settingFn(node)
        if node.display == .none {
            self.frame = NSRect.zero
        }

        if let btn = self as? NSButton, type(of: btn) == NSButton.self {
            btn.isBordered = false
        }
        
        return node
    }
    
    @discardableResult
    func bindAsRoot(to ctx: FLContext, _ settingFn: (FLNode) -> Void = { _ in }) -> FLNode {
        self.wantsLayer = true
        self.layer?.masksToBounds = false
        let node = ctx.associate(view: self, superView: nil, isRoot: true)
        settingFn(node)
        if node.display == .none {
            self.frame = NSRect.zero
        }
        return node
    }
    
    func node(_ ctx: FLContext) -> FLNode? {
        return ctx.getNode(by: self)
    }
}

class FLTextField : NSTextField {
    fileprivate var enableOnFocusNotify = false
    
    override func becomeFirstResponder() -> Bool {
        if self.enableOnFocusNotify, let window = self.window {
            NotificationCenter.default.post(name: FLNotify_OnFocus_Key, object: window, userInfo: ["sender": self])
        }
        return super.becomeFirstResponder()
    }
}

extension NSTextField {
    var flText: String {
        get {
            return self.stringValue
        }
        set {
            self.stringValue = newValue
        }
    }
    
    func startObservrOnFocusEvent() {
        if let input = self as? FLTextField {
            input.enableOnFocusNotify = true
        }
    }
    
    func stopObservrOnFocusEvent() {
        if let input = self as? FLTextField {
            input.enableOnFocusNotify = false
        }
    }
    
    func enableMultipleLineMode() {
        self.cell?.usesSingleLineMode = false
    }
    
    func enableSingleLineMode() {
        self.cell?.usesSingleLineMode = true
    }

}

extension NSTextView {
    var flText: String {
        get {
            return self.string
        }
        set {
            self.string = newValue
        }
    }
}

private var buttonTypeKey = "btn_flButtonType"
extension NSButton {
    var flTitle: String {
        get {
            return self.title
        }
        set {
            self.title = newValue
            self.setTitleColor()
        }
    }
    
    var flTitleColor: NSColor {
        get {
            return self.contentTintColor ?? .clear
        }
        set {
            self.contentTintColor = newValue
            self.setTitleColor()
        }
    }
    
    var flButtonType: ButtonType? {
        get {
            if let objNumber = objc_getAssociatedObject(self, &buttonTypeKey) as? NSNumber {
                return ButtonType(rawValue: objNumber.uintValue)!
            }
            return nil
        }
        set {
            self.setButtonType(newValue!)
            if objc_getAssociatedObject(self, &buttonTypeKey) == nil {
                objc_setAssociatedObject(self, &buttonTypeKey, NSNumber(value: newValue!.rawValue), .OBJC_ASSOCIATION_RETAIN)
            }
        }
    }
    
    private func setTitleColor() {
        if self.flButtonType != nil, let titleColor = self.contentTintColor,
            self.title.count > 0 && self.title != "Button" {
            let title = self.title
            
            let style = NSMutableParagraphStyle()
            style.alignment = .center
            let attrs: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font: self.font ?? NSFont.systemFont(ofSize: 14),
                                                         NSAttributedString.Key.foregroundColor: titleColor,
                                                         NSAttributedString.Key.paragraphStyle: style]
            let attrTitle = NSMutableAttributedString(string: title)
            attrTitle.addAttributes(attrs, range: NSRange(location: 0, length: title.count))
            self.attributedTitle = attrTitle
        }
    }
}

func FLCheckbox() -> NSButton {
    let checkbox = NSButton()
    checkbox.flButtonType = .switch
    return checkbox
}

func FLLabel() -> NSTextField {
    let label = FLTextField()
    label.isEditable = false
    label.enableSingleLineMode()
    label.cell?.wraps = false
    label.drawsBackground = false
    label.isBezeled = false
    return label
}

// TODO: remove, add tapSignal and placeholder args
func FLInputField() -> NSTextField {
    let input = FLTextField()
    input.enableSingleLineMode()
    input.cell?.wraps = false
    input.cell?.isScrollable = true
    return input
}

extension NSTextField {
    func focus() {
        if self.isEditable {
            self.becomeFirstResponder()
            self.currentEditor()?.moveToEndOfLine(nil)
        }
    }
}

func FLShowAlert( _ info: String, doneTitle: String?="OK", cancelTitle: String="Cancel", _ completionFn: Action1<Bool>) {
    let alert = NSAlert()
    alert.messageText = "Warning!"
    alert.informativeText = info
    alert.alertStyle = .warning
    if let doneTitle = doneTitle {
        alert.addButton(withTitle: doneTitle)
    }
    alert.addButton(withTitle: cancelTitle)
    completionFn(alert.runModal() == .cancel)
}

func FLShowCloseAlert(_ info: String, _ completionFn: Action1<Bool>={ _ in }) {
    FLShowAlert(info, doneTitle: nil, completionFn)
}

func FLPopover(_ contentVC: NSViewController) -> NSPopover {
    let popover = NSPopover()
    popover.appearance = NSAppearance(named: .aqua)
    popover.behavior = .applicationDefined
    popover.contentViewController = contentVC
    return popover
}
extension NSPopover {
    func show(_ relativeToView: NSView, _ edge: NSRectEdge) {
        if self.isShown {
            self.close()
        }
        self.show(relativeTo: relativeToView.bounds, of: relativeToView, preferredEdge: edge)
    }
}

extension NSScrollView {
    func scrollToBottom(forVertical: Bool) {
        self.scrollTo(forBottom: true, forVertical: forVertical)
    }
    
    func scrollToTop(forVertical: Bool) {
        self.scrollTo(forBottom: false, forVertical: forVertical)
    }
    
    private func scrollTo(forBottom: Bool, forVertical: Bool) {
        if let contentView = self.contentView.documentView {
            var point = NSPoint(x: 0, y: 0)
            if forBottom {
                if forVertical {
                    point = NSPoint(x: 0, y: self.frame.size.height - contentView.frame.size.height)
                } else {
                    point = NSPoint(x: self.frame.size.width - contentView.frame.size.width, y: 0)
                }
            }
            self.contentView.scroll(point)
        }
    }
}

// REMARK: wrapper NSTableView

protocol FLTableRowCellable {
    static func typeName() -> String
    static func cellIdentifier() -> String
    static func cellHeight(by dataItem: Any) -> CGFloat
    
    func updateData(_ data: Any) -> Void
    func render(_ table: NSTableView) -> Void
    func startMouseOver() -> Void
    func stopMouseOver() -> Void
}

extension FLTableRowCellable {
    static func typeName() -> String {
        return "\(Self.self)"
    }
    
    static func cellIdentifier() -> String {
        return "rowcell_\(Self.typeName())"
    }
}

class VoidRowCell: NSTableCellView, FLTableRowCellable {
    static func cellHeight(by dataItem: Any) -> CGFloat {
        return 0
    }
    
    required init() {
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func updateData(_ data: Any) { }
    func render(_ table: NSTableView) { }
    func startMouseOver() { }
    func stopMouseOver() { }
}

class FLTableView<ItemType, RowCellType: NSTableCellView & FLTableRowCellable> : NSView, NSTableViewDataSource, NSTableViewDelegate {
    private let mutex = Mutex()
    fileprivate let scrollView = NSScrollView()
    fileprivate let tableView = NSTableView()
    fileprivate var isRowCellType = false
    fileprivate var protocolType: FLTableRowCellable.Type!
    fileprivate var rowCellIdentifier: String!
    fileprivate var columnTitles: [String]! = nil
    fileprivate var dataItems: [SectionModel<Void, ItemType>]! = nil
    fileprivate var showLatestDisposer: IODisposer!
    fileprivate var currVerticalPostion: Float = 0
    fileprivate var selectItemCallback: Action1<(ItemType, Int)>?
    fileprivate var currMouseOverCell: FLTableRowCellable? = nil
    fileprivate var currMouseOverCellIndex = -1
    fileprivate var enableOnFocusNotify = false
    
    deinit {
        // fix: not notify `didLiveScrollNotification` after switch script and logger in tab-container
        self.disableAutoShowLatestRows()
    }

    convenience init(_ context: FLContext, isRowCellType: Bool=false) {
        self.init(frame: CGRect.zero)

        self.isRowCellType = isRowCellType
        scrollView.bind(to: context, self) { it in
            it.flexGrow = 1
        }
        scrollView.hasVerticalScroller = true
        scrollView.contentView.documentView = tableView
        
        tableView.dataSource = self
        if self.isRowCellType {
            tableView.delegate = self
            let defaultColumn = NSTableColumn()
            tableView.addTableColumn(defaultColumn)
            tableView.headerView = nil

            let instance = RowCellType()
            self.protocolType = (Mirror(reflecting: instance).subjectType as! FLTableRowCellable.Type)
            let nib = NSNib(nibNamed: NSNib.Name.init(protocolType.typeName()), bundle: nil)
            self.rowCellIdentifier = protocolType.cellIdentifier()
            tableView.register(nib, forIdentifier: NSUserInterfaceItemIdentifier(rawValue: rowCellIdentifier))
        }
        // TODO: remove，add enable flag
        let trackingArea = NSTrackingArea(rect: scrollView.frame,
                                          options: [.mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow, .inVisibleRect],
                                          owner: self, userInfo: nil)
        scrollView.addTrackingArea(trackingArea)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func mouseExited(with event: NSEvent) {
        if let cell = self.currMouseOverCell {
            cell.stopMouseOver()
            self.currMouseOverCell = nil
            self.currMouseOverCellIndex = -1
        }
    }
    
    override func mouseMoved(with event: NSEvent) {
        guard let dataItems = self.dataItems, dataItems.count > 0 && dataItems[0].items.count > 0 else {
            return
        }
        let pointInTableView = tableView.convert(event.locationInWindow, to: nil)
        let index = tableView.row(at: pointInTableView)
        if index != self.currMouseOverCellIndex, let cell = self.currMouseOverCell {
            cell.stopMouseOver()
        }
        
        var cell: FLTableRowCellable? = nil
        if index != -1 {
            cell = tableView.view(atColumn: 0, row: index, makeIfNecessary: false) as? FLTableRowCellable
        }
        self.currMouseOverCell = cell
        self.currMouseOverCellIndex = index
        cell?.startMouseOver()
    }
    
    var rac_eventSelectItem: IOMonad<(ItemType, Int)> {
        return IOMonad {[weak self] thunker in
            self?.selectItemCallback = { val in
                thunker.sendNext(val)
            }
            return {
                self?.selectItemCallback = nil
            }
        }
    }
    
    func setColumnTitles(titles: [(String, Float, Float)]) {
        var columnTitles = [String]()
        for (column, minWidth, maxWidth) in titles {
            columnTitles.append(column)
            let tableColumn = NSTableColumn(identifier: .init("column_\(column)"))
            tableColumn.title = column
            if minWidth > 0 {
                tableColumn.minWidth = CGFloat(minWidth)
            } else if maxWidth > 0 {
                tableColumn.maxWidth = CGFloat(maxWidth)
            }
            self.tableView.addTableColumn(tableColumn)
        }
        self.columnTitles = columnTitles
    }
    
    fileprivate func setData(columnData: [SectionModel<Void, ItemType>]?) {
        if let dataItems = columnData {
            self.dataItems = dataItems
        } else {
            var dataItems = [SectionModel<Void, ItemType>]()
            for title in self.columnTitles {
                let item = buildSection([ItemType](), title: title)
                dataItems.append(item)
            }
            self.dataItems = dataItems
        }
    }
    
    fileprivate func setData(rowData: [ItemType]?) {
        if let items = rowData, isRowCellType {
            self.dataItems = [buildSection(items)]
        }
    }
    
    func cleanData(needReload: Bool) {
        let succ = self.mutex.tryLock()
        for item in self.dataItems {
            item.items.removeAll()
        }
        if needReload {
            self.tableView.reloadData()
        }
        if succ {
            self.mutex.unlock()
        }
    }
    
    func appendData(rowDataItem: ItemType) {
        let succ = self.mutex.tryLock()
        if self.isRowCellType {
            self.appendData(columnDataItem: [rowDataItem])
        }
        if succ {
            self.mutex.unlock()
        }
    }
    
    func appendData(columnDataItem: [ItemType]) {
        let succ = self.mutex.tryLock()
        if self.isRowCellType {
            if let dataItems = self.dataItems.first?.items {
                var items = [ItemType](dataItems)
                items.append(contentsOf: columnDataItem)
                let data = buildSection(items)
                self.reload(columnData: [data])
            }
        } else {
            if self.columnTitles.count == columnDataItem.count {
                var columnData = [String : ItemType]()
                for idx in 0..<self.columnTitles.count {
                    let title = columnTitles[idx]
                    let value = columnDataItem[idx]
                    columnData[title] = value
                }
                self.appendData(columnDatas: [columnData])
            }
        }
        if succ {
            self.mutex.unlock()
        }
    }
    
    func appendData(columnDatas: [[String: ItemType]]) {
        let succ = self.mutex.tryLock()
        let dataItem = self.dataItems
        let titlesCount = self.columnTitles.count
        for i in 0..<titlesCount {
            let title = self.columnTitles[i]
            let section = dataItems[i]
            for data in columnDatas {
                if data.count == titlesCount {
                    if let value = data[title] {
                        section.items.append(value)
                    } else {
                        continue
                    }
                }
            }
        }
        self.reload(columnData: dataItem)
        if succ {
            self.mutex.unlock()
        }
    }
    
    func reload(columnData: [SectionModel<Void, ItemType>]? = nil, rowData: [ItemType]?=nil) {
        let succ = self.mutex.tryLock()
        if let columnData = columnData {
            self.setData(columnData: columnData)
        }
        if let rowData = rowData {
            self.setData(rowData: rowData)
        }
        self.tableView.reloadData()
        
        if self.showLatestDisposer != nil && self.currVerticalPostion > 0.93 {
            self.scrollToLatestRow()
        }
        if succ {
            self.mutex.unlock()
        }
    }
    
    func scrollToLatestRow() {
        if let row = self.dataItems?[0].items.count, row > 0 {
            self.tableView.scrollRowToVisible(row-1)
        }
    }
    
    func enableAutoShowLatestRows() {
        if self.showLatestDisposer == nil {
            self.scrollToLatestRow()
            self.currVerticalPostion = 0.99
            self.showLatestDisposer = NotificationCenter.default
                .rac_addObserver(notifyName: NSScrollView.didLiveScrollNotification,
                                 object: self.scrollView)
                .subscribeNext {[weak self] notify in
                    if let self = self, let verticalScroller = self.scrollView.verticalScroller,
                       let documentView = self.scrollView.documentView {
                       let clipView = self.scrollView.contentView
                        let offset = Float((clipView.bounds.origin.y + clipView.bounds.height) / documentView.bounds.height)
                        let postion = verticalScroller.floatValue < offset ? offset : verticalScroller.floatValue
                        self.currVerticalPostion = postion
                    }
                }
        }
    }
    
    func disableAutoShowLatestRows() {
        if let disposer = self.showLatestDisposer {
            disposer.dispose()
            self.showLatestDisposer = nil
        }
    }
    
    func startObservrOnFocusEvent() {
        self.enableOnFocusNotify = true
    }
    
    func stopObservrOnFocusEvent() {
        self.enableOnFocusNotify = false
    }
    
    // REMARK: NSTableView DataSource
    
    func numberOfRows(in tableView: NSTableView) -> Int {
        if let dataItems = self.dataItems, dataItems.count > 0 {
            let number = self.dataItems[0].items.count
            return number
        } else {
            return 0
        }
    }
    
    func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
        if !self.isRowCellType, let dataItems = self.dataItems, dataItems.count > 0 {
            for section in self.dataItems {
                if section.title == tableColumn?.title && section.items.count > row {
                    return section.items[row]
                }
            }
        }
        return nil
    }
    
    // REMARK: NSTableView Delegate
    
    func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
        if self.isRowCellType, let items = self.dataItems.first?.items, items.count > 0 {
            return protocolType.cellHeight(by: items[row])
        }
        return 44
    }

    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
        if self.isRowCellType, let items = self.dataItems.first?.items, items.count > 0 {
            if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: rowCellIdentifier),
                                             owner: self.tableView) as? RowCellType {
                cell.updateData(items[row])
                cell.render(tableView)
                return cell
            }
        }
        return nil
    }
   
    func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
        if self.isRowCellType, let callback = self.selectItemCallback,
            let items = self.dataItems.first?.items, items.count > 0 {
            let item = items[row]
            callback((item, row))
        }
        if self.enableOnFocusNotify, let window = tableView.window {
            NotificationCenter.default.post(name: FLNotify_OnFocus_Key, object: window, userInfo: ["sender": tableView])
        }
        return true
    }
}

func buildColumnsTableView<ItemType>(_ context: FLContext,
                                     data: [SectionModel<Void, ItemType>]?=nil,
                                     columnTitles: [(String, Float, Float)]=[]) -> FLTableView<ItemType, VoidRowCell> {
    let table = FLTableView<ItemType, VoidRowCell>(context)
    if columnTitles.count > 0 {
        table.setColumnTitles(titles: columnTitles)
        for idx in 0..<columnTitles.count {
            let (title, _, _) = columnTitles[idx]
            if let dataItems = data, dataItems.count > idx {
                let section = dataItems[idx]
                if section.title.isEmpty {
                    section.title = title
                }
            }
        }
    }
    if table.columnTitles == nil, let dataItems = data {
        var columnTitles = [(String, Float, Float)]()
        for section in dataItems {
            if let title = section.title {
                columnTitles.append((title, 0, 0))
            } else {
                break
            }
        }
        if columnTitles.count > 0 {
            table.setColumnTitles(titles: columnTitles)
        }
    }
    table.setData(columnData: data)
    if table.dataItems != nil {
        table.reload()
    }
    return table
}

func buildRowCellTableView<ItemType, RowCellType: NSTableCellView & FLTableRowCellable>(_ context: FLContext,
                                                                                        _ cellType: RowCellType.Type,
                                                                                        data: [ItemType]?=nil)
    -> FLTableView<ItemType, RowCellType> {
    let table = FLTableView<ItemType, RowCellType>(context, isRowCellType: true)
    
    table.setData(rowData: data)
    if table.dataItems != nil {
        table.reload()
    } else {
        table.dataItems = [buildSection([ItemType]())]
    }
    return table
}

// REMARK: FLViewController

class FLViewController : NSViewController {
    var loadByStoryBoard = false
    var isInited = false
    var hasSetWindowStyle = false
    var monitorRender = false
    var hadMonitor = false
    var yoga: FLContext! = nil
    private var rac_subject: IOSubject<Bool>? = nil
    var rac_appearState: IOMonad<Bool> {
        if self.rac_subject == nil {
            self.rac_subject = IOSubject<Bool>()
        }
        return self.rac_subject!.signal
    }
    
    convenience init(monitorRender: Bool=false) {
        self.init(FLContext(), monitorRender: monitorRender)
    }
    
    convenience init(_ yoga: FLContext, monitorRender: Bool=false) {
        self.init(nibName: nil, bundle: nil)
        self.yoga = yoga
        self.monitorRender = monitorRender
    }
    
    override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    final override func loadView() {
        self.view = NSView()
        // load by storyboard
        if self.yoga == nil {
            self.yoga = FLContext()
            self.setViewStyle()
            self.loadByStoryBoard = true
        }
    }
    
    final override func viewDidAppear() {
        super.viewDidAppear()
        if !hasSetWindowStyle, let window = self.view.window {
            self.setWindowStyle(currWindow: window)
            hasSetWindowStyle = true
        }
        if let subject = self.rac_subject {
            subject.sendNext(true)
        }
    }
    
    final override func viewDidDisappear() {
        super.viewDidDisappear()
        if let subject = self.rac_subject {
            subject.sendNext(false)
        }
    }
    
    final override func viewDidLayout() {
        super.viewDidLayout()
        if !isInited {
            isInited = true
            // observer yoga willRender & didiRender
            if self.monitorRender {
                self.startMonitorRender()
            }
            if !hasSetWindowStyle, let window = self.view.window {
                self.setWindowStyle(currWindow: window)
                hasSetWindowStyle = true
            }
            if loadByStoryBoard {
                self.view.bindAsRoot(to: self.yoga)
                self.layoutUI(needsLayout: false)
            }
        }
    }
    
    final func startMonitorRender() {
        if !self.hadMonitor {
            self.hadMonitor = true
            self.yoga.allowObserver()
            self.yoga.$didRenderTrigger.signal()
                .start(with: self.yoga.didRender)
                .take(until: self.rac_willDealloc)
                .subscribeNext {[weak self] val in
                    if let self = self, val {
                        self.didLayoutUI()
                    }
                }
            self.yoga.$willRenderTrigger.signal()
                .take(until: self.rac_willDealloc)
                .subscribeNext {[weak self] val in
                    if let self = self, val {
                        self.willRefreshUI()
                    }
            }
        }
        self.monitorRender = true
    }
    
    func setWindowStyle(currWindow: NSWindow) { }
    func setViewStyle() { }
    // @param needsLayout: used by refresh(), args: passed by refresh(args:)
    func layoutUI(needsLayout: Bool, args: [String : Any?]? = nil) { }
    func didLayoutUI() { }
    func willRefreshUI() { }
    func didRemovedFromParent() { }
    
    final func addTo(superView: NSView, args: [String : Any?]?=nil, _ setRootNodeFn: Action1<FLNode>? = nil) {
        if let customFn = setRootNodeFn {
            self._addTo(superView: superView, needsLayout: false, args: args, customFn)
        } else {
            self._addTo(superView: superView, needsLayout: false, args: args)
        }
    }
    
    private func _addTo(superView: NSView, needsLayout: Bool, args: [String : Any?]?, _ setRootNodeFn: (FLNode) -> Void = {it in it.flexGrow = 1}) {
        self.view.bind(to: yoga, superView) { it in
            setRootNodeFn(it)
        }
        // load by parent viewcontroller
        self.layoutUI(needsLayout: needsLayout, args: args)
    }

    @discardableResult
    final func removeFromSuperView() -> NSView? {
        var superView = self.view.superview
        if let parent = superView, type(of: parent) != NSView.self {
            superView = nil
        }
        yoga.removeNode(by: self.view, ignoreParent: superView == nil)
        self.didRemovedFromParent()
        return superView
    }
    
    final func refresh(args: [String : Any?]? = nil) {
        self.yoga.willRenderView()
        if let superView = self.removeFromSuperView() {
            self._addTo(superView: superView, needsLayout: true, args: args)
        } else {
            self.layoutUI(needsLayout: true, args: args)
        }
        self.yoga.apply()
    }
}

// REMARK: FLTabContainer

class TabContainerAnimator : NSObject { }

extension TabContainerAnimator: NSViewControllerPresentationAnimator {
    func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController) {
        if let fromVC = fromViewController as? FLViewController, let vc = viewController as? FLViewController {
            let superView = fromViewController.view.superview!
            fromVC.removeFromSuperView()
            vc.addTo(superView: superView)
            vc.yoga.apply()
        }
    }
    
    func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController) {

    }
}

class FLTabContainer : NSView {
    var childViewControlers = [FLViewController]()
    var buttonTitles = [String]()
    var container = NSView()
    var currVC: FLViewController? = nil
    var nextVC: FLViewController? = nil
    var presentVC: FLViewController? = nil
    
    convenience init(_ yoga: FLContext, vcList: [FLViewController], buttonTitles: [String] = []) {
        self.init(frame: CGRect.zero)
        for item in vcList {
            self.childViewControlers.append(item)
        }
        for item in buttonTitles {
            self.buttonTitles.append(item)
        }
        // buttons span
        let buttonsSpan = NSView()
        buttonsSpan.bind(to: yoga, self) { it in
            it.direction = .row
            it.height = 36
            it.alignItems = .center
            it.display = buttonTitles.count > 0 ? .flex : .none
        }
        buttonsSpan.flBackgroundColor = .lightGray
        // container
        self.container.bind(to: yoga, self) { it in
            it.flexGrow = 1
        }
        // first vc
        if let vc = self.childViewControlers.first {
            vc.addTo(superView: self.container)
            self.currVC = vc
            self.nextVC = self.getNextVC()
        }
        // bind to buttons
        for index in 0..<self.buttonTitles.count {
            let title = self.buttonTitles[index]
            let btn = NSButton()
            btn.bind(to: yoga, buttonsSpan) { it in
                it.marginLeft = 4
                it.height = 26
                it.minWidth = 80
            }
            btn.flCornerRadius = 4
            btn.flBackgroundColor = .darkGray
            btn.flTitle = title
            btn.flTitleColor = .white
            btn.font = NSFont.systemFont(ofSize: 13)
            btn.rac_eventTouch.subscribeNext {[weak self] _ in
                if let self = self {
                    if self.buttonTitles.count == 1 {
                        self.nextTab()
                    } else {
                        self.nextTab(index: index)
                    }
                }
            }
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    private func getNextVC() -> FLViewController? {
        if self.childViewControlers.count > 1, let currVC = self.currVC,
           let index = self.childViewControlers.firstIndex(of: currVC) {
            let endIndex = self.childViewControlers.endIndex
            var nextIndex = index.advanced(by: 1)
            if nextIndex >= endIndex {
                nextIndex = self.childViewControlers.startIndex
            }
            return self.childViewControlers[nextIndex]
        }
        return nil
    }
    
    func nextTab(index: Int = -1) {
        if let currVC = self.currVC, var nextVC = self.nextVC {
            // avoid tab to current tab
            if index >= 0, self.childViewControlers[index] == currVC {
                return
            }
            if let presentVC = self.presentVC {
                if currVC != presentVC {
                    currVC.dismiss(presentVC)
                } else {
                    nextVC.dismiss(presentVC)
                }
            }
            // find nextVC with index
            if index >= 0, self.childViewControlers[index] != nextVC {
                nextVC = self.childViewControlers[index]
            }
            currVC.present(nextVC, animator: TabContainerAnimator())
            self.presentVC = nextVC
            self.currVC = nextVC
            self.nextVC = self.getNextVC()
        }
    }
    
    func pushVC(_ vc: FLViewController) {
        // TODO: implement like UINavigationViewController
    }
    
    func popupVC() {
        
    }
}

extension NSImage {
    func resized(to newSize: NSSize) -> NSImage? {
        if self.size == newSize {
            return self
        }
        if let bitmapRep = NSBitmapImageRep(
            bitmapDataPlanes: nil, pixelsWide: Int(newSize.width), pixelsHigh: Int(newSize.height),
            bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
            colorSpaceName: .calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0
        ) {
            bitmapRep.size = newSize
            NSGraphicsContext.saveGraphicsState()
            NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: bitmapRep)
            draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: .zero, operation: .copy, fraction: 1.0)
            NSGraphicsContext.restoreGraphicsState()

            let resizedImage = NSImage(size: newSize)
            resizedImage.addRepresentation(bitmapRep)
            return resizedImage
        }
        return nil
    }
}

#else

extension UIView {
    static func scaleOfScreen() -> Float {
        return Float(UIScreen.main.scale)
    }
    
    @discardableResult
    func bind(to ctx: FLContext, _ superView: UIView, _ settingFn: (FLNode) -> Void = { _ in }) -> FLNode {
        let node = ctx.associate(view: self, superView: superView)
        settingFn(node)
        if node.display == .none {
            self.frame = CGRect.zero
        }
        return node
    }
    
    @discardableResult
    func bindAsRoot(to ctx: FLContext, _ settingFn: (FLNode) -> Void = { _ in }) -> FLNode {
        let node = ctx.associate(view: self, superView: nil, isRoot: true)
        settingFn(node)
        if node.display == .none {
            self.frame = CGRect.zero
        }
        return node
    }
    
    func node(_ ctx: FLContext) -> FLNode? {
        return ctx.getNode(by: self)
    }
    
    var isFLContentNode: Bool {
        return self is UILabel || self is UIButton || self is UIInputView || self is UITextField || self is UITextView || self is UIImageView
    }
}

#endif
